import math
import os
import random
import time

import numpy
import openpyxl
import xlwt
from sklearn import preprocessing

random.seed(0)


def read_data(dir_str):
    '''
    读取txt文件中的数据
    数据内容：科学计数法保存的多行多列数据
    输入：txt文件的路径
    输出：小数格式的数组，行列与txt文件中相同
    '''
    data_temp = []
    with open(dir_str) as fdata:
        while True:
            line = fdata.readline()
            if not line:
                break
            data_temp.append([float(i) for i in line.split()])
    return numpy.array(data_temp)


def randome_init_train_test(data, n_tr):
    ''' 随机划分训练集和测试集 '''
    # sklearn提供一个将数据集切分成训练集和测试集的函数train_test_split
    train_index = numpy.random.choice(data.shape[0], size=n_tr, replace=False, p=None)
    train_data = data[train_index]
    test_index = numpy.delete(numpy.arange(data.shape[0]), train_index)  # 删除train_index对应索引的行数
    test_data = data[test_index]
    return train_data, test_data


def min_max_normalization(np_array):
    ''' 离差标准化，(Xi-min(X))/(max(X)-min(X)) '''
    min_max_scaler = preprocessing.MinMaxScaler()
    ret = min_max_scaler.fit_transform(np_array)
    return ret


def label_to_value(label):
    ''' 标签转换为对应输出值 (由于输出层结构，需要修改输出数据结构)'''
    switch = {
        0.0: [1, 0, 0, 0],
        1.0: [0, 1, 0, 0],
        2.0: [0, 0, 1, 0],
        3.0: [0, 0, 0, 1]
    }
    return switch[label]


def value_to_label(value):
    ''' 神经网络输出值转换为对应标签 '''
    return value.index(max(value))


def rand(min, max):
    ''' 随机取[a, b]范围内的值 '''
    return (max - min) * random.random() + min


def make_matrix(m, n, fill=0.0):  # 生成多维矩阵
    mat = []
    for i in range(m):
        mat.append([fill] * n)
    return mat


def sigmoid(x):
    if x < -709:
        return 1.0 / (1.0 + math.exp(709))
    return 1.0 / (1.0 + math.exp(-x))


def sigmoid_derivative(x):
    return x * (1 - x)


class BPNeuralNetwork:
    def __init__(self):  # 设置在BP神经网络中用到的参数
        self.input_n = 0
        self.hidden_n = 0
        self.output_n = 0
        self.input_values = []  # [1.0] * self.input_n
        self.hidden_values = []  # [1.0] * self.hidden_n
        self.output_values = []  # [1.0] * self.output_n
        self.input_weights = []
        self.output_weights = []
        self.input_correction = []  # dw1
        self.output_correction = []  # dw2
        self.input_bias = []
        self.output_bias = []

    def setup(self, ni, nh, no):  # 参数设置
        self.input_n = ni
        self.hidden_n = nh
        self.output_n = no
        # init
        self.input_values = [1.0] * self.input_n  # 输入层神经元输出（输入特征）
        self.hidden_values = [1.0] * self.hidden_n  # 中间层神经元输出
        self.output_values = [1.0] * self.output_n  # 隐藏层神经元输出（预测结果）
        if os.path.exists(path + 'input_weights.txt') and \
                os.path.exists(path + 'output_weights.txt') and \
                os.path.exists(path + 'input_correction.txt') and \
                os.path.exists(path + 'output_correction.txt') and \
                os.path.exists(path + 'input_bias.txt') and \
                os.path.exists(path + 'output_bias.txt'):
            self.input_weights = read_data(path + 'input_weights.txt')
            self.output_weights = read_data(path + 'output_weights.txt')
            self.input_correction = read_data(path + 'input_correction.txt')
            self.output_correction = read_data(path + 'output_correction.txt')
            self.input_bias = read_data(path + 'input_bias.txt')
            self.output_bias = read_data(path + 'output_bias.txt')
            print('已读取数据文件')
        else:
            self.input_weights = make_matrix(self.input_n, self.hidden_n)
            self.output_weights = make_matrix(self.hidden_n, self.output_n)
            # 初始随机赋值，在范围[-1, +1]内
            for i in range(self.input_n):
                for h in range(self.hidden_n):
                    self.input_weights[i][h] = rand(-1, 1)
            for h in range(self.hidden_n):
                for o in range(self.output_n):
                    self.output_weights[h][o] = rand(-1, 1)
            self.input_correction = make_matrix(self.input_n, self.hidden_n)
            self.output_correction = make_matrix(self.hidden_n, self.output_n)
            self.input_bias = [0.0] * self.input_n
            self.output_bias = [0.0] * self.output_n

    def predict(self, inputs):  # 前向传播（在train中套在反向传播的train前面）
        # 输入层计算
        for i in range(self.input_n - 1):
            self.input_values[i] = inputs[i]
        # 隐藏层计算
        for j in range(self.hidden_n):
            total = 0.0
            for i in range(self.input_n):
                total += self.input_values[i] * self.input_weights[i][j]
            self.hidden_values[j] = sigmoid(total + self.input_bias[j])
        # 输出层计算
        for k in range(self.output_n):
            total = 0.0
            for j in range(self.hidden_n):
                total += self.hidden_values[j] * self.output_weights[j][k]
            self.output_values[k] = sigmoid(total + self.output_bias[k])
        return self.output_values[:]

    def back_propagate(self, case, label, learn, correct):
        # 前向预测
        self.predict(case)
        # 计算输出层的误差 w2
        output_deltas = [0.0] * self.output_n
        for o in range(self.output_n):
            error = label[o] - self.output_values[o]
            output_deltas[o] = sigmoid_derivative(self.output_values[o]) * error
        # 计算隐藏层的误差 w1
        hidden_deltas = [0.0] * self.hidden_n
        for h in range(self.hidden_n):
            error = 0.0
            for o in range(self.output_n):
                error += output_deltas[o] * self.output_weights[h][o]
            hidden_deltas[h] = sigmoid_derivative(self.hidden_values[h]) * error
        # 更新隐藏-输出层权重 b2
        for h in range(self.hidden_n):
            for o in range(self.output_n):
                change = output_deltas[o] * self.hidden_values[h]
                self.output_weights[h][o] += learn * change + correct * self.output_correction[h][o]
                self.output_correction[h][o] = change
                self.output_bias[o] += learn * change
        # 更新输入-隐藏层权重 b1
        for i in range(self.input_n):
            for h in range(self.hidden_n):
                change = hidden_deltas[h] * self.input_values[i]
                self.input_weights[i][h] += learn * change + correct * self.input_correction[i][h]
                self.input_correction[i][h] = change
                self.input_bias[i] += learn * change

        # 计算样本的均方误差
        error = 0.0
        for o in range(len(label)):
            error += 0.5 * (label[o] - self.output_values[o]) ** 2
        return error

    def train(self, datas, labels, epoch=5000, learn=0.05, correct=0.1, stop_error=0.001):
        for j in range(epoch):
            error = 0.0
            for i in range(len(datas)):
                label = labels[i]
                data = datas[i]
                error += self.back_propagate(data, label, learn, correct)
            if error <= stop_error:
                return j + 1
            print(
                f"用时:{time.strftime('%d %H:%M:%S', time.gmtime((time.time() - start_time)))} "
                f"平均准确率：{'%0.4f' % numpy.mean(accuracy) if times != 1 else 0} "
                f"{hidden_nodes}节点第{times}轮第{j}次:error={error}")
        return epoch


def read_excel(name):
    wb = openpyxl.load_workbook(name+'.xlsx')
    sheet = wb.active
    res = list(sheet.rows)
    rdata = []
    for j in range(sheet.max_row-1):
        rdata.append([s.value for s in res[j+1]][0:9])
    return numpy.array(rdata)


times = 1
accuracy = []
filename = input('请输入训练集文件名称：')
while True:
    if os.path.exists(filename+'.xlsx'):
        break
    print("无法找到文件！")
    filename = input('请重新输入训练集文件名称：')
if __name__ == '__main__':
    n_tr = 0.75     # 训练集所占比例
    input_nodes = 8     # 输入层节点数
    hidden_nodes = int(input("请输入节点数："))  #
    path = f"{filename}\\{hidden_nodes}\\"
    if not os.path.exists(filename):
        os.mkdir(filename)
    if not os.path.exists(path):
        os.mkdir(path)
    if os.path.exists('data_mode.txt'):
        os.system(f"copy data_mode.txt {path}data_mode.txt")
        print('已搜索到运行配置文件data_mode.txt')
    else:
        print('未搜索到运行配置文件data_mode.txt，可添加后重新运行脚本')
    os.system(f"copy {filename+'.xlsx'} {path}test.xlsx")

    output_nodes = 4    # 输出层节点数
    epochs = 500    # 单轮矫正次数
    learn_rate = 0.5  # 学习率
    momentum_rate = 0.09  # 动量参数
    correct_rate = 0.1  # 矫正率
    start_time = time.time()
    nn = BPNeuralNetwork()
    nn.setup(input_nodes, hidden_nodes, output_nodes)  # 设置BP神经网络框架

    data = read_excel(filename)
    normal_data = min_max_normalization(data[:, 1:])  # 变量归一化
    # nor_k = []
    # nor_b = []
    # for i in range(8):
    #     nor_k.append(1 / (data[:, i+1].max() - data[:, i+1].min()))
    #     nor_b.append(data[:, 1].min() / (data[:, 1].max() - data[:, 1].min()))
    # numpy.savetxt(path + r'nor_k.txt', nor_k, fmt='%s')
    # numpy.savetxt(path + r'nor_b.txt', nor_b, fmt='%s')
    # print(read_data(path + r'nor_k.txt'))
    # print(read_data(path + r'nor_b.txt'))
    # while True:
    #     a = 0

    data = numpy.concatenate((data[:, :1], normal_data), axis=1)  # 取出输出的结果和标准化的数据拼接在一起
    print(f"已读取训练集数据{data.shape[0]}行")
    input('按回车继续')

    while True:
        tr, te = randome_init_train_test(data, round(data.shape[0]*n_tr))  # 随机划分训练集和测试集
        tr_in = tr[:, 1:]
        tr_out = [label_to_value(v) for v in tr[:, 0]]  # 由于输出层使用多个节点，需要修改输出数据结构
        n_true = 0  # 统计正确预测数量

        st = time.perf_counter()
        epoch = nn.train(tr_in, tr_out, epochs, learn_rate, correct_rate)
        print('epoch:', epoch, '\nTrain_time:', time.perf_counter() - st)
        pre = []
        for t in te:
            t_in = t[1:]
            label = value_to_label(nn.predict(t_in))
            if label == t[0]:
                n_true += 1
            pre.append([label])

        # 输出统计结果(取最近10轮平均值)
        if times >= 10:     # 列表已满
            del accuracy[0]     # 出列
            m = n_true / (data.shape[0] * (1 - n_tr))
            accuracy.append(m)  # 入列
            # print(m)
        else:
            m = n_true / (data.shape[0] * (1 - n_tr))
            accuracy.append(m)  # 入列
            # print(m)

        numpy.savetxt(path + r'input_bias.txt', nn.input_bias, fmt='%s')
        numpy.savetxt(path + r'output_bias.txt', nn.output_bias, fmt='%s')
        numpy.savetxt(path + r'input_weights.txt', nn.input_weights, fmt='%s')
        numpy.savetxt(path + r'output_weights.txt', nn.output_weights, fmt='%s')
        numpy.savetxt(path + r'input_correction.txt', nn.input_correction, fmt='%s')
        numpy.savetxt(path + r'output_correction.txt', nn.output_correction, fmt='%s')

        times = times + 1
